library TextTag
/*------------------------------
|                               |
|  TextTag                      |
|      By MyPad                 |
|                               |
|  Version:                     |
|      v.1.4.10                 |
|--------------------------------------------------------------------------------
|
|   TextTag is a library that provides convenience for the creation of local
|   text tags. Knowing that there is a limit to the number of text tag handles
|   that can be created and used at any given time, this system optimizes
|   the usage of these limited text tag handles, minimizing their actual amount.
|   
|   If you want to create localized text tags, then this library is definitely
|   for you. If you want an object-oriented approach to handling text tags,
|   this library should suit your needs.
|
|---------------------------------------------------------------------------------
|           |
|  API      |
|           |
 --------------------------------------------------------------------------------
*/
//! novjass
struct TextTag
    Creation and destruction:
    
    TextTag.create(integer playerId) -> TextTag
        //  Creates a local texttag for a certain player.
        
        //  Note that the playerId parameter takes the actual number of the player,
        //  not the player itself
    
        //  To create a global instance, request bj_MAX_PLAYER_SLOTS or higher
    local TextTag tag = TextTag.create(integer playerId) -> TextTag
        //  Creates a TextTag instance.
    ..
    
    tag.destroy()
        //  This destroys a TextTag instance. How it destroys an instance
        //  for a player is determined either manually
        //  or automatically
    
    method operator/s:
        //  Note: "==" operators indicate getters, while "=" operators indicate setters.
        tag.z       == real z
        //  Returns the member z
        tag.height2 == real z
        //  Returns z as well for BC
        tag.height2 = real height
        //  Internally calls method setPos()
        
        tag.height == real size
        //  Returns the member size
        tag.height = real size
        //  sets the size of a Text Tag
        
        tag.permanent == boolean isPermanent
        //  Returns the member isPermanent
        tag.permanent = boolean bool
        //  Sets the permanence of a TextTag
        //  If set to true, this will lock the transparency of
        //  the TextTag. Be careful when using.
        // Documented in v.1.4.10
        tag.fade == real fadepoint
        //  Returns the member fadepoint
        tag.fade = real fadepoint
        //  Determines the amount of time before the TextTag
        //  starts fading. (Watch out for the permanence property!)
        
        tag.duration == real dur
        //  Returns the member dur -> tells duration.
        tag.duration = real newDur
        //  If it is not permanent, a TextTag's duration is reset to the requested (r).
        
        tag.msg or tag.message == string s
        //  message is just an abstraction of msg, a method operator that returns a member s.
        
        tag.msg = string sometext
        tag.message = string sometext
        //  Sets the message of a texttag
        
        tag.red = integer int(0 - 255)
        tag.green = integer int(0 - 255)
        tag.blue = integer int(0 - 255)
        tag.alpha = integer int(0 - 255)
        //  Returns the red, green, blue, and alpha properties of the text tag.
        
        //  Alpha levels determine the visibility of the locally created text tag.
        //  255 means the instance is fully visible while 0 means transparent.
            
        tag.widget = widget wid
        tag.unit = unit uni
        //  Attaches a text tag to a widget or a unit.
            
    method-s:
        //tag
        
        tag.setPosPrime(real x, real y, real heightOffset, boolean break)
            //  Sets the position of a TextTag instance to the requested coordinates
            //  with a z-offset denoted as heightOffset
        
        tag.setPos(real x, real y, real heightOffset)
            //  Calls setPosPrime with a false parameter for variable break.
        tag.setVeloc(real xvel, real yvel)
            //  Sets the pace at which the tag moves at a certain direction.
        
        tag.setVelocEx(real speed, real angle)
            //  Wrapper function for setVeloc
            
        tag.generate(integer playerId) -> TextTag
            //  Generates the same text tag for another player
        
        tag.transfer(integer playerId) -> TextTag
            //  Same as generate, but destroys the base instance.
        
        tag.applyForceVisibility(force f, boolean visible)
        tag.visible_to_force(force f, boolean b) // @deprecated
            //  Only works when the Text Tag instance is a global one.
            //  Sets the visibility of a global text tag to flag b,
            //  towards players belonging to a certain force f
            
endstruct
function CreateTextTagBJ(player p, real x, real y, real offset, integer red, integer green, integer blue, string msg) -> {TextTag}
function CreateTextTagVJ(player p, real x, real y, real offset, integer red, integer green, integer blue, string msg) -> {TextTag}
    //  Creates a TextTag instance with the default settings.
    //  If you're not planning on saving the instance immediately,
    //  you can still access it via vj_lastCreatedTextTag
    {vj_lastCreatedTextTag} // or return vj_lastCreatedTextTag
    
//! endnovjass
//! novjass
    Update History:
    
    v.1.4.1.0 - Slightly changed the hashing algorithm used in GetHashID.
                - Exchanged execution speed for guaranteed unique indices
                  for the internal timers.
              - Revised the API to describe the machinations of the system
                in more detail.
              - The method visible_to_force has been marked for deprecation.
                You are advised to use the method applyForceVisibility instead.
                - Author's note: Turns out I forgot to include applyForceVisibility
                  as one of the publicly accessible methods.
    v.1.4.0.0 - Removed all requirements
    v.1.3.0.6 - Added some details on method operator getters. (Denoted by ==)
                - Fixed setVeloc-s slightly buggy behavior. (It didn-t originally pause the texttag when set to 0)
                - Exposed a new method setPosPrime (does what setPos did before, with a boolean parameter)
                - setPos now calls setPosPrime with a false flag.
                
    v.1.3.0.5 - Improved API documentation.
                - Added a new method operator height2
                - Backwards compatible!
                
    v.1.3.0.4 - Update history created!
                - method operator unit= fixed.
                
//! endnovjass
globals
    //  This determines the check rate of the timers.
    private constant real INTERVAL  = 1/16.
endglobals
private function print takes string msg returns nothing
    call BJDebugMsg(msg)
endfunction
private module TextTagModule
    private  static constant integer    HASH_DIVISOR        = 37
    readonly static constant integer    ALLOC_SPACE         = 101
    readonly static constant real       DEFAULT_HEIGHT      = 10.
    readonly static constant real       HEIGHT_OFFSET       = 0.023 / 10.
            //  Default offset value for velocity
    readonly static constant real       VELOCITY_OFFSET     = 0.071 / 128.
    //  Default duration of a texttag instance
    readonly static constant real       DEFAULT_DURATION    = 2.     
    //  Default speed and angle of a texttag
    readonly static constant real       DEFAULT_VELOC_SPEED = 80.
    readonly static constant real       DEFAULT_VELOC_ANGLE = 90.
    private static timer    array       decayWatcher
    private static integer  array       decayWatchMap
    private static thistype array       activeList
    private static integer  array       timerId
    private thistype                    allocId
    //  Flag members
    private boolean is_active
    private boolean is_permanent
    //  Real members
    private real    fadepoint
    private real    size
    private real    dur
    private real    cx
    private real    cy
    private real    cz
    private real    dx
    private real    dy
    //  Message
    private string  str
    //  Color members
    private integer r
    private integer g
    private integer b
    private integer a
    private static method GetHashID takes timer whichtimer returns integer
        local integer id        = GetHandleId(whichtimer)
        local integer baseIndex = ModuloInteger(ModuloInteger(id, JASS_MAX_ARRAY_SIZE), HASH_DIVISOR)
        local integer iterIndex = 0
        if (decayWatchMap[baseIndex] == 0) or (decayWatchMap[baseIndex] == id) then
            if (decayWatchMap[baseIndex] == 0) then
                set decayWatchMap[baseIndex] = id
            endif
            return baseIndex
        endif
        set iterIndex       = baseIndex
        loop
            set iterIndex   = ModuloInteger(iterIndex + 1, HASH_DIVISOR)
            if (decayWatchMap[iterIndex] == 0) or (decayWatchMap[iterIndex] == id) then
                if (decayWatchMap[iterIndex] == 0) then
                    set decayWatchMap[iterIndex] = id
                endif
                return iterIndex
            endif
            exitwhen (iterIndex == baseIndex)
        endloop
        return -1
    endmethod
    private static method SetTimerID takes timer whichtimer, integer id returns nothing
        set timerId[GetHashID(whichtimer)]   = id
    endmethod
    private static method GetTimerID takes timer whichtimer returns integer
        return timerId[GetHashID(whichtimer)]
    endmethod
    private static method ApplyPlayerCondition takes integer pIndex returns boolean
        local player p  = null
        if pIndex < bj_MAX_PLAYER_SLOTS then
            set p = Player(pIndex)
        endif
        return (p == null) or (GetLocalPlayer() == p)
    endmethod
    private static method allocate takes integer i returns thistype
        local integer  j    = 0
        local thistype head = thistype(0)
        local thistype this = thistype(0)
        set head    = thistype(i*ALLOC_SPACE)
        set this    = head.allocId
        if (this.allocId == head) or (this.allocId == thistype(0)) then
            set j               = integer(this) + 1
            if j - integer(head) >= ALLOC_SPACE then
                return thistype(-1)
            endif
            set this            = thistype(j)
            set head.allocId    = this
        else
            //  Obtaining a recycled instance
            set head.allocId    = this.allocId
            set this.allocId    = 0
        endif
        set this.is_active      = true
        return this
    endmethod
    private method deallocate takes nothing returns nothing
        local integer mod   = ModuloInteger(integer(this), ALLOC_SPACE)
        local thistype head
        if not this.is_active then
            return
        endif
        if mod == 0 then
            return
        endif
        if this.allocId != thistype(0) then
            return
        endif
        set head            = thistype(integer(this)/ALLOC_SPACE*ALLOC_SPACE)
        set this.allocId    = head.allocId
        set head.allocId    = this
    endmethod
    method destroy takes nothing returns nothing
        local integer   pIndex  = integer(this)/ALLOC_SPACE
        local thistype  head    = thistype(pIndex*ALLOC_SPACE)
        local thistype  that    = thistype(0)
        local integer   i       = activeList[integer(head)]
        local integer   max     = i
        if not this.is_active then
            return
        endif
        if ApplyPlayerCondition(pIndex) then
            call DestroyTextTag(this.tag)
            set this.tag    = null
        endif
        call this.deallocate()
        set this.target         = null
        set this.widgetTarg     = null
        set this.is_permanent   = false
        set this.fadepoint      = 0.
        set this.size           = 0.
        set this.dur            = 0.
        set this.dx             = 0.
        set this.dy             = 0.
        set this.cx             = 0.
        set this.cy             = 0.
        set this.cz             = 0.
        set this.r              = 0
        set this.g              = 0
        set this.b              = 0
        set this.a              = 0
        set this.str            = null
        loop
            exitwhen i < 1
            set that    = activeList[integer(head) + i]
            if that == this then
                set that                            = activeList[integer(head) + max]
                set activeList[integer(head) + i]   = that
                set activeList[integer(head) + max] = 0
                set activeList[integer(head)]       = max - 1
                exitwhen true
            endif
            set i       = i - 1
        endloop
    endmethod
    
    //============================================================//
    //          Methods                                           //
    //============================================================//
    
    method setPosPrime takes real x, real y, real heightOffset, boolean detach returns nothing
        local integer pIndex        = integer(this)/ALLOC_SPACE
        if detach then
            set this.target         = null
            set this.widgetTarg    = null
        endif
        if this.target != null then
            set heightOffset = heightOffset - GetUnitFlyHeight(this.target)
        endif
        set this.cx     = x
        set this.cy     = y
        set this.cz     = heightOffset
        if ApplyPlayerCondition(pIndex) then
            if (not (this.dx != 0)) and (not (this.dy != 0)) then
                call SetTextTagPos(this.tag, x, y, heightOffset)
                return
            endif
            call DestroyTextTag(this.tag)
            set this.tag = CreateTextTag()
            call SetTextTagPos(this.tag, x, y, heightOffset)
            call SetTextTagText(this.tag, this.str, this.size*HEIGHT_OFFSET)
            call SetTextTagColor(this.tag, this.r, this.g, this.b, this.a)
            
            call SetTextTagPermanent(this.tag, this.is_permanent)
            if not this.is_permanent then
                call SetTextTagLifespan(this.tag, this.dur)
                call SetTextTagFadepoint(this.tag, RMaxBJ(this.fadepoint, 0))
            endif
        endif
    endmethod
    
    method setPos takes real x, real y, real heightOffset returns nothing
        call setPosPrime(x, y, heightOffset, false)
    endmethod
    
    method setVeloc takes real xvel, real yvel returns nothing
        local integer pIndex = integer(this)/ALLOC_SPACE
        if ApplyPlayerCondition(pIndex) then
            if xvel != 0 or yvel != 0 then
                call SetTextTagVelocity(this.tag, xvel*VELOCITY_OFFSET, yvel*VELOCITY_OFFSET)
            else
                call this.setPos(this.cx, this.cy, this.cz)
            endif
        endif
        set this.dx     = xvel
        set this.dy     = yvel
    endmethod
    
    method setVelocEx takes real speed, real angle returns nothing
        call this.setVeloc(Cos(angle*bj_DEGTORAD)*speed, Sin(angle*bj_DEGTORAD)*speed)
    endmethod
    
    //  Sets visibility of a global text tag to a certain flag towards a certain force.
    method applyForceVisibility takes force f, boolean b returns nothing
        local integer pIndex = integer(this)/ALLOC_SPACE
        if pIndex < bj_MAX_PLAYER_SLOTS then
            return
        endif
        if IsPlayerInForce(GetLocalPlayer(), f) then
            call SetTextTagVisibility(this.tag, b)
        endif
    endmethod
    // @deprecated
    method visible_to_force takes force f, boolean b returns nothing
        call BJDebugMsg("TextTag:visible_to_force >> This will be deprecated in later versions. Use applyForceVisibility instead.")
        call this.applyForceVisibility(f, b)
    endmethod
    //============================================================//
    //          Method operators                                  //
    //============================================================//
    
    method operator permanent takes nothing returns boolean
        return this.is_permanent
    endmethod
    method operator permanent= takes boolean flag returns nothing
        local integer pIndex    = integer(this)/ALLOC_SPACE
        set this.is_permanent   = flag
        if not ApplyPlayerCondition(pIndex) then
            return
        endif
        call SetTextTagPermanent(this.tag, flag)
    endmethod
    
    method operator duration takes nothing returns real
        if this.is_permanent then
            return -1.
        endif
        return this.dur
    endmethod
    method operator duration= takes real r returns nothing
        local integer pIndex    = integer(this)/ALLOC_SPACE
        set this.dur            = r
        if not ApplyPlayerCondition(pIndex) then
            return
        endif
        call SetTextTagLifespan(this.tag, r)
    endmethod
    
    method operator msg takes nothing returns string
        return this.str
    endmethod
    method operator msg= takes string str returns nothing
        local integer pIndex    = integer(this)/ALLOC_SPACE
        set this.str            = str
        if not ApplyPlayerCondition(pIndex) then
            return
        endif
        call SetTextTagText(this.tag, this.str, this.size*HEIGHT_OFFSET)
    endmethod
    
    //  These methods are wrappers...
    method operator message takes nothing returns string
        return this.str
    endmethod
    method operator message= takes string str returns nothing
        set this.msg    = str
    endmethod
    
    //  A newer method operator that actually returns the height of the text tag.
    method operator z takes nothing returns real
        return this.cz
    endmethod
    method operator z= takes real new returns nothing
        local integer pIndex    = integer(this)/ALLOC_SPACE
        if this.widgetTarg != null then
            call this.setPos(GetWidgetX(this.widgetTarg), GetWidgetY(this.widgetTarg), new)
        elseif this.target != null then
            call this.setPos(GetUnitX(this.target), GetUnitY(this.target), new)
        else
            call this.setPos(cx, cy, new)
        endif
    endmethod
    
    method operator height2 takes nothing returns real
        return this.cz
    endmethod
    method operator height2= takes real new returns nothing
        set this.z = new
    endmethod
    
    //  Cannot deal away with backwards compatibility here..
    method operator height takes nothing returns real
        return this.size
    endmethod
    method operator height= takes real h returns nothing
        local integer pIndex    = integer(this)/ALLOC_SPACE
        set this.size           = h
        if not ApplyPlayerCondition(pIndex) then
            return
        endif
        call SetTextTagText(this.tag, this.str, this.size*HEIGHT_OFFSET)
    endmethod
    
    method operator fade takes nothing returns real
        return this.fadepoint
    endmethod
    method operator fade= takes real fader returns nothing
        local integer pIndex    = integer(this)/ALLOC_SPACE
        if this.is_permanent then
            return
        endif
        set this.fadepoint      = RMinBJ(RAbsBJ(fader), this.dur)
        if not ApplyPlayerCondition(pIndex) then
            return
        endif
        call SetTextTagFadepoint(this.tag, fadepoint)
    endmethod
    
    method operator red takes nothing returns integer
        return this.r
    endmethod
    method operator red= takes integer value returns nothing
        local integer pIndex    = integer(this)/ALLOC_SPACE
        set this.r              = value
        if not ApplyPlayerCondition(pIndex) then
            return
        endif
        call SetTextTagColor(this.tag, this.r, this.g, this.b, 255 - this.a)
    endmethod
    
    method operator green takes nothing returns integer
        return this.g
    endmethod
    method operator green= takes integer value returns nothing
        local integer pIndex    = integer(this)/ALLOC_SPACE
        set this.g              = value
        if not ApplyPlayerCondition(pIndex) then
            return
        endif
        call SetTextTagColor(this.tag, this.r, this.g, this.b, 255 - this.a)
    endmethod
    
    method operator blue takes nothing returns integer
        return this.b
    endmethod
    method operator blue= takes integer value returns nothing
        local integer pIndex    = integer(this)/ALLOC_SPACE
        set this.b              = value
        if not ApplyPlayerCondition(pIndex) then
            return
        endif
        call SetTextTagColor(this.tag, this.r, this.g, this.b, 255 - this.a)
    endmethod
    
    method operator alpha takes nothing returns integer
        return this.a
    endmethod
    method operator alpha= takes integer value returns nothing
        local integer pIndex    = integer(this)/ALLOC_SPACE
        set this.a              = value
        if not ApplyPlayerCondition(pIndex) then
            return
        endif
        call SetTextTagColor(this.tag, this.r, this.g, this.b, 255 - this.a)
    endmethod
    
    method operator widget takes nothing returns widget
        return this.widgetTarg
    endmethod
    method operator widget= takes widget wid returns nothing
        set this.target     = null
        set this.widgetTarg = wid
        if wid  != null then
            call this.setVeloc(0,0)
            call this.setPos(GetWidgetX(wid), GetWidgetY(wid), this.cz)
        endif
    endmethod
    
    method operator unit takes nothing returns unit
        return this.target
    endmethod
    method operator unit= takes unit u returns nothing
        set this.widgetTarg = null
        set this.target     = u
        if u != null then
            call this.setVeloc(0,0)
            call this.setPos(GetUnitX(u), GetUnitY(u), this.cz)
        endif
    endmethod
    
    method operator texttag takes nothing returns texttag
        return this.tag
    endmethod
    //  =================================   //
    //  Create and onTick methods           //
    //  =================================   //
    private method onTickUpdate takes nothing returns boolean
        if this.widgetTarg != null then
            call this.setPos(GetWidgetX(this.widgetTarg), GetWidgetY(this.widgetTarg), this.cz)
        elseif this.target != null then
            call this.setPos(GetUnitX(this.target), GetUnitY(this.target), this.cz)
        else
            set this.cx = this.cx + this.dx*INTERVAL
            set this.cy = this.cy + this.dy*INTERVAL
        endif
        
        if this.is_permanent then
            return true
        endif
        set this.dur        = this.dur - INTERVAL
        set this.fadepoint  = this.fadepoint - INTERVAL
        if this.dur <= 0. then
            call this.destroy()
            return false
        endif
        return true
    endmethod
    private static method onTick takes nothing returns nothing
        local integer   pIndex  = GetTimerID(GetExpiredTimer())
        local integer   i       = 1
        local thistype  head    = thistype(pIndex*ALLOC_SPACE)
        local thistype  this    = 0
        loop
            exitwhen i > integer(activeList[integer(head)])
            set this    = activeList[integer(head) + i]
            if not this.onTickUpdate() then
                set i   = i - 1
            endif
            set i       = i + 1
        endloop
        if activeList[integer(head)] <= 0 then
            call PauseTimer(decayWatcher[pIndex])
        endif
    endmethod
    
    static method create takes integer pIndex returns thistype
        local thistype this     = 0
        local thistype head     = 0
        local integer  index    = 0
        local player   p        = null
        if pIndex < 0 or pIndex > bj_MAX_PLAYER_SLOTS then
            set pIndex  = bj_MAX_PLAYER_SLOTS
        endif
        set head = thistype(pIndex*ALLOC_SPACE) 
        set this = thistype.allocate(pIndex)
        if this == thistype(-1) then
            return this
        endif
        set index = activeList[integer(head)] + 1           
        set activeList[integer(head)]          = index
        set activeList[integer(head) + index]  = this
        if ApplyPlayerCondition(pIndex) then
            set this.tag    = CreateTextTag()
        endif
        if activeList[integer(head)] == 1 then
            call TimerStart(decayWatcher[pIndex], INTERVAL, true, /*
                            */ function thistype.onTick)
        endif
        return this
    endmethod
    
    //  Copies a Text Tag instance to another player
    method generate takes integer playerId returns thistype
        local thistype new      = 0
        local integer pIndex    = integer(this)/ALLOC_SPACE
        
        if pIndex == playerId then
            return this
        endif
        
        set new = thistype.create(playerId)
        if new == thistype(-1) then
            return new
        endif
        
        set new.duration    = this.dur
        set new.fade        = this.fadepoint
        set new.permanent   = this.is_permanent
        set new.height      = this.size
        set new.red         = this.r
        set new.green       = this.g
        set new.blue        = this.b
        set new.alpha       = this.a
        set new.msg         = this.msg
        call new.setPos(this.cx, this.cy, this.cz)
        call new.setVeloc(this.dx, this.dy)
        return new
    endmethod
    //  Same as the method generate but destroys the base instance
    method transfer takes integer playerId returns thistype
        local thistype that = this.generate(playerId)
        if that == -1 then
            return this
        endif
        call this.destroy()
        return that
    endmethod

    private static method onInit takes nothing returns nothing
        local integer i     = 0
        local thistype head
        loop
            exitwhen i > bj_MAX_PLAYER_SLOTS
            set head                        = thistype(i*ALLOC_SPACE)
            set head.allocId                = head
            set activeList[integer(head)]   = 0
            set decayWatcher[i]             = CreateTimer()
            call SetTimerID(decayWatcher[i], i)
            set i   = i + 1
        endloop
    endmethod
endmodule
struct TextTag extends array
    readonly widget  widgetTarg
    readonly unit    target
    readonly texttag tag
    implement TextTagModule
endstruct
globals
    TextTag vj_lastCreatedTextTag   = 0
endglobals
function CreateTextTagBJ takes player p, real x, real y, real offset, integer red, integer green, integer blue, string msg returns TextTag
    local TextTag this      = TextTag(0)
    local integer pIndex    = bj_MAX_PLAYER_SLOTS
    if p != null then
        set pIndex  = GetPlayerId(p)
    endif
    set this        = TextTag.create(pIndex)
    if this == TextTag(-1) then
        return this
    endif
    set this.duration       = TextTag.DEFAULT_DURATION
    set this.fade           = TextTag.DEFAULT_DURATION - 0.5
    set this.height         = TextTag.DEFAULT_HEIGHT
    set this.permanent      = false
    set this.red            = red
    set this.green          = green
    set this.blue           = blue
    set this.alpha          = 0
    set this.msg            = msg
    call this.setPos(x, y, offset)
    call this.setVelocEx(TextTag.DEFAULT_VELOC_SPEED, TextTag.DEFAULT_VELOC_ANGLE)
    set vj_lastCreatedTextTag = this
    return vj_lastCreatedTextTag
endfunction
function CreateTextTagVJ takes player p, real x, real y, real offset, integer red, integer green, integer blue, string msg returns TextTag
    return CreateTextTagBJ(p, x, y, offset, red, green, blue, msg)
endfunction
endlibrary